package zyf.jmidifile;

import java.io.Serializable;
import java.util.LinkedList;

public final class JMidiTempoMap implements Serializable {
    public static final int DEFAULT_BPM = 120;
    private static final int DEFAULT_BEAT_UNIT = 4;

    private final TempoPoint[] tempoPoints;
    private final int tpq;

    public JMidiTempoMap(final JMidiTrack track, int tpq) {
        // 添加默认的 Tempo
        LinkedList<TempoPoint> tempoPointList = new LinkedList<TempoPoint>();
        TempoPoint lastTempoPoint = new TempoPoint();
        tempoPointList.add(lastTempoPoint);
        TempoPoint newTempoPoint;
        this.tpq = tpq;

        // 遍历 track 找出来所有的 tempo event 和 tempo signature
        for (JMidiEvent e : track.events) {
            if (e.getEventType() != JMidiEvent.TYPE_META) {
                continue;
            }

            int metaType = e.getMetaType();
            if (metaType == JMidiEvent.TYPE_META_TEMPO) {
                newTempoPoint = updateTempoPoint(e, e.getBPM(), lastTempoPoint, tpq);
                if (newTempoPoint != null) {
                    lastTempoPoint = newTempoPoint;
                    tempoPointList.add(newTempoPoint);
                }
            }
        }

        // 转换到 tempoPoints 数组里面
        tempoPoints = new TempoPoint[tempoPointList.size()];
        tempoPointList.toArray(tempoPoints);
    }

    public long ticksToMs(int tick) {
        TempoPoint tempoPoint = findTempoPointByTick(tempoPoints, tick);
        if (tempoPoint == null) return -1;

        return tempoPoint.ms + JMidiUtils.ticksToMs(tick - tempoPoint.tick, tpq, tempoPoint.bpm, DEFAULT_BEAT_UNIT);
    }

    public int msToTicks(long ms) {
        TempoPoint tempoPoint = findTempoPointByMs(tempoPoints, ms);
        if (tempoPoint == null) return -1;
        return tempoPoint.tick + JMidiUtils.msToTicks(ms - tempoPoint.ms, tpq, tempoPoint.bpm, DEFAULT_BEAT_UNIT);
    }

    /**
     * 将外部时间转换成 midi event 内部使用的 ms。两种时间可能存在毫秒级别的误差
     */
    public long msToInnerMs(long ms) {
        return ticksToMs(msToTicks(ms));
    }

    private static TempoPoint updateTempoPoint(final JMidiEvent e, int bpm, TempoPoint lastTempoPoint, int tpq) {
        TempoPoint tp = null;
        if (e.ms < 0) {
            // 通过 tick 算 ms
            if (e.tick == lastTempoPoint.tick) {
                lastTempoPoint.bpm = bpm;
                return null;
            }
            tp = new TempoPoint();
            tp.bpm = bpm;
            tp.ms = lastTempoPoint.ms + JMidiUtils.ticksToMs(e.tick - lastTempoPoint.tick, tpq, lastTempoPoint.bpm, DEFAULT_BEAT_UNIT);
        } else {
            // 通过 ms 算 tick
            if (e.ms == lastTempoPoint.ms) {
                lastTempoPoint.bpm = bpm;
                return null;
            }
            tp = new TempoPoint();
            tp.bpm = bpm;
            tp.tick = lastTempoPoint.tick + JMidiUtils.msToTicks(e.ms - lastTempoPoint.ms, tpq, lastTempoPoint.bpm, DEFAULT_BEAT_UNIT);
        }

        return tp;
    }

    private static class TempoPoint {
        int bpm = DEFAULT_BPM;
        int tick;
        long ms;
    }

    private static TempoPoint findTempoPointByTick(final TempoPoint[] tempoPoints, int tick) {
        if (tick < tempoPoints[0].tick) return null;

        int low = 0;
        int high = tempoPoints.length;

        while (low < high) {
            int mid = (low + high) >>> 1;
            if (tick < tempoPoints[mid].tick) {
                high = mid;
            } else {
                low = mid + 1;
            }
        }
        return (low > 0) ? tempoPoints[low - 1] : null;
    }

    private static TempoPoint findTempoPointByMs(final TempoPoint[] tempoPoints, long ms) {
        if (ms < tempoPoints[0].ms) return null;

        int low = 0;
        int high = tempoPoints.length;

        while (low < high) {
            int mid = (low + high) >>> 1;
            if (ms < tempoPoints[mid].ms) {
                high = mid;
            } else {
                low = mid + 1;
            }
        }
        return (low > 0) ? tempoPoints[low - 1] : null;
    }

}
